package com.gitee.feizns.web.ex;

import com.gitee.feizns.ArrayUtils;
import com.gitee.feizns.StringUtils;
import com.gitee.feizns.reflect.ClassUtils;
import com.gitee.feizns.reflect.ConstructorUtils;
import com.gitee.feizns.web.boot.ex.GlobalExceptionProperties;
import com.gitee.feizns.web.domain.result.Err;
import com.gitee.feizns.web.domain.result.Ret;
import com.gitee.feizns.web.utils.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @author feizns
 * @since 2019/6/13
 */
@RestControllerAdvice
public class GlobalRestfulExceptionAdvice {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalRestfulExceptionAdvice.class);

    /**
     *
     */
    private static GlobalExceptionProperties properties;

    public GlobalRestfulExceptionAdvice(GlobalExceptionProperties properties) {
        this.properties = properties;
    }

    /**
     * 获取异常处理器
     * @param ex
     * @return
     */
    public CustomizeExceptionHandler<? extends Exception, ?> getExceptionHandler(Class<?> ex) {
        Class<? extends CustomizeExceptionHandler> handler = optimal(ex);
        return handler != null
                ? ConstructorUtils.newInstance(handler)
                : null;
    }

    private Class<? extends CustomizeExceptionHandler> optimal(Class<?> ex) {
        Map<Class<?>, Class<? extends CustomizeExceptionHandler>> handlerMap = properties.getExceptionHandlerMap();
        Class[] retKeys = handlerMap.keySet().stream().filter(item -> item.isAssignableFrom(ex)).toArray(Class[]::new);
        if ( ArrayUtils.isEmpty(retKeys) == false ) {
            Queue<Class<?>> classes = ClassUtils.getClassesAndInterfaces(ex);
            while ( classes.isEmpty() == false ) {
                Class<?> aClass = classes.remove();
                for (Class retKey : retKeys) {
                    if ( retKey == aClass )
                        return handlerMap.get(retKey);
                }
            }
        }
        return null;
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity exceptionHandle(Exception e) {
        if ( properties.isDebug() )
            e.printStackTrace();

        Throwable prevCause = e;
        Throwable cause = e;
        while ( cause != null ) {
            Class<? extends Throwable> causeClass = cause.getClass();

            CustomizeExceptionHandler handler = getExceptionHandler(causeClass);
            if ( handler != null ) {
                if ( LOGGER.isDebugEnabled() )
                    LOGGER.debug("FOUND CustomizeExceptionHandler => " + handler.getClass());
                return logResult(cause, handler.handle(cause));
            }

            ResponseEntity mappingRet = errorMappingHandle(cause);
            if ( mappingRet != null )
                return mappingRet;

            prevCause = cause;
            cause = cause.getCause();
        }

        return logResult(prevCause, internalServerError(prevCause));
    }

    public static final ResponseEntity errorMappingHandle(Throwable e) {
        return errorMappingHandle(e, err -> ResponseEntity.ok(Ret.err(err)));
    }

    public static final ResponseEntity errorMappingHandle(Throwable e, Function<Err, ResponseEntity> supplier) {
        Err err = properties.getErrMap().get(e.getClass());
        if ( err != null )
            return logResult(e, supplier.apply(err));
        return null;
    }

    private ResponseEntity internalServerError(Throwable e) {
        String message = e.getMessage();

        if ( message == null )
            message = StringUtils.toUnderline(e.getClass().getSimpleName()).toUpperCase();

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Ret.err(HttpStatus.INTERNAL_SERVER_ERROR.value(), message));
    }

    private static ResponseEntity logResult(Throwable cause, ResponseEntity ret) {
        HttpServletRequest req = WebUtils.getReq();

        if ( LOGGER.isDebugEnabled() )
            LOGGER.debug(String.format("[%s : %s] IP: %s ==> MESSAGE [ %s ] EXCEPTION[ %s ] ",
                    req.getMethod(),
                    req.getServletPath(),
                    WebUtils.getIpAddress(),
                    cause.getMessage()),
                    cause.getClass()
            );

        return ret;
    }

}
